package tablature;

import data.UserPreferences;
import gui.TablatureWindow;
import java.util.ArrayList;
import javax.sound.midi.MidiChannel;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Synthesizer;
import kit.Component;
import midi.MidiController;
import midi.MidiSubject;

/**
 * Class: Metronome
 * Description: A metronome to run in the background.
 * This is a modified version of Dave Bricetti's metronome
 * with the GUI and MIDI stripped out.
 * The original source can be found here:
 * http://briccetti.blogspot.com/2008/04/simple-java-midi-metronome.html
 * @author et
 */
public class Tablature
{

    private ArrayList <Component> components;
    private ArrayList <Measure> measures;

    private MidiSubject midiController;

    private Thread metronomeThread; // New thread each time the metronome is turned on
    private long timeBetweenBeats;
    private boolean keepPlaying = true;

    // Get channel 10 which is reserved for percussion.
    private MidiChannel channel = null;

    private boolean soundOn = false;
    private TimeSignature timeSignature;


    private int velocity = 127;

    private int beatInMeasure = 0;
    private int noteInMeasure = -1;
    private int measureNum = 0;


    private TablatureWindow tabWindow = TablatureWindow.getInstance();

    private static Tablature instance;

    public static Tablature getInstance()
    {
        if (instance == null)
        {
            instance = new Tablature();
        }
        return instance;
    }

    private Tablature()
    {
        
    }


    /**
     * Sets the tempo. May be called while the metronome is on.
     * @param beatsPerMinute the tempo, in beats per minute
     */
    public void start(int beatsPerMinute, boolean soundOn, TimeSignature timeSignature)
    {
        components = UserPreferences.getInstance().getComponents();
        midiController = new MidiController();
        registerComponents(true);


        this.soundOn = soundOn;
        this.timeSignature = timeSignature;

        System.out.println("Playing at " + beatsPerMinute + "BPM");

        beatsPerMinute *= 4;

        timeBetweenBeats = 1000 * 60 / beatsPerMinute;
        

        if (soundOn)
        {
            try
            {
                final Synthesizer synthesizer = MidiSystem.getSynthesizer();
                synthesizer.open();
                channel = synthesizer.getChannels()[9];

            }
            catch (MidiUnavailableException ex)
            {
                System.out.println("Could not activate sound with the metronome.");
                System.out.println("Try closing any applications that might be using your sound card.");
                soundOn = false;
            }
        }


        Measure curMeasure = constructDefaultMeasure();
        measures = new ArrayList <Measure>();
        measures.add(curMeasure);
        tabWindow.setText(measures.get(measureNum).toString());


        if (soundOn)
        {
            metronomeThread = new Thread(new ThreadWithSound(), "Metronome");
            metronomeThread.setPriority(Thread.MAX_PRIORITY);
            metronomeThread.start();
        }
        else
        {
            metronomeThread = new Thread(new ThreadWithOutSound(), "Metronome");
            metronomeThread.setPriority(Thread.MAX_PRIORITY);
            metronomeThread.start();
        }
    }

    /**
     * Registers or unregisters
     * all the components to listen to MIDI device.
     * @param listen - True to register components, False to unregister.
     */
    private void registerComponents(boolean listen)
    {
        midiController.refreshDevice();
        for (Component c : components)
        {
            if (listen)
            {
                midiController.registerObserver(c);
            }
            else
            {
                midiController.removeObserver(c);
            }
        }
    }

    private class ThreadWithSound implements Runnable
    {

        public void run()
        {
            final long startTime = System.currentTimeMillis();

            while (keepPlaying)
            {
                noteInMeasure++;

                


                if (noteInMeasure == 16)
                {
                    Measure curMeasure = constructDefaultMeasure();
                    measures.add(curMeasure);

                    beatInMeasure = 0;
                    noteInMeasure = 0;
                    measureNum++;
                }

                if ((noteInMeasure) % 4 == 0)
                {
                    channel.noteOn(56, velocity);
                    beat();
                    beatInMeasure++;

                    final long currentTimeBeforeSleep = System.currentTimeMillis();
                    final long currentLag = (currentTimeBeforeSleep - startTime) % timeBetweenBeats;
                    final long sleepTime = timeBetweenBeats - currentLag;
                    try
                    {
                        Thread.sleep(sleepTime);
                        
                    } catch (InterruptedException ex)
                    {
                        System.out.println("Interrupted");
                    }
                    channel.noteOff(56);
                     
                }
                else
                {

                    final long currentTimeBeforeSleep = System.currentTimeMillis();
                    final long currentLag = (currentTimeBeforeSleep - startTime) % timeBetweenBeats;
                    final long sleepTime = timeBetweenBeats - currentLag;
                    try
                    {
                        Thread.sleep(sleepTime);
                    } catch (InterruptedException ex)
                    {
                        System.out.println("Interrupted");
                    }
                }

               


            }
        }

        public void beat()
        {
            int curMeasureNum = getMeasureNum();
            int curBeat = getBeatInMeasure();
            int curNote = getNoteInMeasure();

            Measure curMeasure = measures.get(curMeasureNum);

            BarLine bl = curMeasure.getTimingBar();


            bl.setNote(curNote, 'X');

            if (getBeatInMeasure() > 0)
            {
                bl.setNote(curNote - 4, timeSignature.getTimingLine().charAt(curNote - 4));
            }

            curMeasure.setTimingBar(bl);

            measures.set(curMeasureNum, curMeasure);

            tabWindow.setText(measures.get(curMeasureNum).toString());
        }

    }

    private class ThreadWithOutSound implements Runnable
    {

        public void run()
        {
            final long startTime = System.currentTimeMillis();

            while (keepPlaying)
            {
                noteInMeasure++;




                if (noteInMeasure == 16)
                {
                    Measure curMeasure = constructDefaultMeasure();
                    measures.add(curMeasure);

                    beatInMeasure = 0;
                    noteInMeasure = 0;
                    measureNum++;
                }

                if ((noteInMeasure) % 4 == 0)
                {
                    beat();
                    beatInMeasure++;

                    final long currentTimeBeforeSleep = System.currentTimeMillis();
                    final long currentLag = (currentTimeBeforeSleep - startTime) % timeBetweenBeats;
                    final long sleepTime = timeBetweenBeats - currentLag;
                    try
                    {
                        Thread.sleep(sleepTime);

                    } catch (InterruptedException ex)
                    {
                        System.out.println("Interrupted");
                    }

                }
                else
                {

                    final long currentTimeBeforeSleep = System.currentTimeMillis();
                    final long currentLag = (currentTimeBeforeSleep - startTime) % timeBetweenBeats;
                    final long sleepTime = timeBetweenBeats - currentLag;
                    try
                    {
                        Thread.sleep(sleepTime);
                    } catch (InterruptedException ex)
                    {
                        System.out.println("Interrupted");
                    }
                }




            }
        }

        public void beat()
        {
            int curMeasureNum = getMeasureNum();
            int curBeat = getBeatInMeasure();
            int curNote = getNoteInMeasure();

            Measure curMeasure = measures.get(curMeasureNum);

            BarLine bl = curMeasure.getTimingBar();


            bl.setNote(curNote, 'X');

            if (getBeatInMeasure() > 0)
            {
                bl.setNote(curNote - 4, timeSignature.getTimingLine().charAt(curNote - 4));
            }

            curMeasure.setTimingBar(bl);

            measures.set(curMeasureNum, curMeasure);

            tabWindow.setText(measures.get(curMeasureNum).toString());
        }

    }

    public void hit(String componentShortName, char hitNote)
    {
        int curMeasureNum = getMeasureNum();
        int curNote = getNoteInMeasure();

        Measure curMeasure = measures.get(curMeasureNum);
        int barLineNum = curMeasure.getBarLinePos(componentShortName);
        BarLine bl = curMeasure.getBarLine(barLineNum);
        bl.setNote(curNote, hitNote);

        curMeasure.setBarLine(barLineNum, bl);
        measures.set(curMeasureNum, curMeasure);
        tabWindow.setText(curMeasure.toString());
    }

    private synchronized int getMeasureNum()
    {
        return measureNum;
    }

    private synchronized void setMeasureNum(int measureNum)
    {
        this.measureNum = measureNum;
    }

    private synchronized int getBeatInMeasure()
    {
        return beatInMeasure;
    }

    private synchronized void setBeatInMeasure(int beatInMeasure)
    {
        this.beatInMeasure = beatInMeasure;
    }

    private synchronized int getNoteInMeasure()
    {
        return noteInMeasure;
    }

    private synchronized void setNoteInMeasure(int noteInMeasure)
    {
        this.noteInMeasure = noteInMeasure;
    }
    




    /**
     * Stops the metronome.
     */
    public void stop()
    {
        keepPlaying = false;
        if (metronomeThread != null)
        {
            metronomeThread.interrupt(); // Interrupt the sleep
        }
    }



    /**
     * Makes the default measure to be added each time we need
     * to add a new one.
     * @return
     */
    private Measure constructDefaultMeasure()
    {

        // Construct blank measure.
        Measure measure = new Measure(timeSignature);

        for (Component component : components)
        {
            // Make a new barline with this component and the given
            // time signature.
            // Then add it to the current measure.
            BarLine barLine = new BarLine(component.getShortName(), timeSignature);
            measure.addBarLine(component.getShortName(), barLine);
        }

        BarLine barLine = new BarLine("", timeSignature);
        measure.addBarLine("", barLine);

        return measure;
    }

    public ArrayList getAllMeasures()
    {
        return measures;
    }
 
}
